<?php
/**
 * @package php-font-lib
 * @link    https://github.com/PhenX/php-font-lib
 * @author  Fabien Ménager <fabien.menager@gmail.com>
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 */

namespace isszz\captcha\font\lib\truetype;

use isszz\captcha\font\lib\AdobeFontMetrics;
use isszz\captcha\font\lib\Font;
use isszz\captcha\font\lib\BinaryStream;
use isszz\captcha\font\lib\table\Table;
use isszz\captcha\font\lib\table\DirectoryEntry;
use isszz\captcha\font\lib\table\type\Glyf;
use isszz\captcha\font\lib\table\type\Name;
use isszz\captcha\font\lib\table\type\NameRecord;

/**
 * TrueType font file.
 *
 * @package php-font-lib
 */
class File extends BinaryStream
{
	/**
	 * @var Header
	 */
	public $header = [];

	private $tableOffset = 0; // Used for TTC

	private static $raw = false;

	protected $directory = [];
	protected $data = [];

	protected $glyph_subset = [];

	public $glyph_all = [];

	static $macCharNames = [
		".notdef", ".null", "CR",
		"space", "exclam", "quotedbl", "numbersign",
		"dollar", "percent", "ampersand", "quotesingle",
		"parenleft", "parenright", "asterisk", "plus",
		"comma", "hyphen", "period", "slash",
		"zero", "one", "two", "three",
		"four", "five", "six", "seven",
		"eight", "nine", "colon", "semicolon",
		"less", "equal", "greater", "question",
		"at", "A", "B", "C", "D", "E", "F", "G",
		"H", "I", "J", "K", "L", "M", "N", "O",
		"P", "Q", "R", "S", "T", "U", "V", "W",
		"X", "Y", "Z", "bracketleft",
		"backslash", "bracketright", "asciicircum", "underscore",
		"grave", "a", "b", "c", "d", "e", "f", "g",
		"h", "i", "j", "k", "l", "m", "n", "o",
		"p", "q", "r", "s", "t", "u", "v", "w",
		"x", "y", "z", "braceleft",
		"bar", "braceright", "asciitilde", "Adieresis",
		"Aring", "Ccedilla", "Eacute", "Ntilde",
		"Odieresis", "Udieresis", "aacute", "agrave",
		"acircumflex", "adieresis", "atilde", "aring",
		"ccedilla", "eacute", "egrave", "ecircumflex",
		"edieresis", "iacute", "igrave", "icircumflex",
		"idieresis", "ntilde", "oacute", "ograve",
		"ocircumflex", "odieresis", "otilde", "uacute",
		"ugrave", "ucircumflex", "udieresis", "dagger",
		"degree", "cent", "sterling", "section",
		"bullet", "paragraph", "germandbls", "registered",
		"copyright", "trademark", "acute", "dieresis",
		"notequal", "AE", "Oslash", "infinity",
		"plusminus", "lessequal", "greaterequal", "yen",
		"mu", "partialdiff", "summation", "product",
		"pi", "integral", "ordfeminine", "ordmasculine",
		"Omega", "ae", "oslash", "questiondown",
		"exclamdown", "logicalnot", "radical", "florin",
		"approxequal", "increment", "guillemotleft", "guillemotright",
		"ellipsis", "nbspace", "Agrave", "Atilde",
		"Otilde", "OE", "oe", "endash",
		"emdash", "quotedblleft", "quotedblright", "quoteleft",
		"quoteright", "divide", "lozenge", "ydieresis",
		"Ydieresis", "fraction", "currency", "guilsinglleft",
		"guilsinglright", "fi", "fl", "daggerdbl",
		"periodcentered", "quotesinglbase", "quotedblbase", "perthousand",
		"Acircumflex", "Ecircumflex", "Aacute", "Edieresis",
		"Egrave", "Iacute", "Icircumflex", "Idieresis",
		"Igrave", "Oacute", "Ocircumflex", "applelogo",
		"Ograve", "Uacute", "Ucircumflex", "Ugrave",
		"dotlessi", "circumflex", "tilde", "macron",
		"breve", "dotaccent", "ring", "cedilla",
		"hungarumlaut", "ogonek", "caron", "Lslash",
		"lslash", "Scaron", "scaron", "Zcaron",
		"zcaron", "brokenbar", "Eth", "eth",
		"Yacute", "yacute", "Thorn", "thorn",
		"minus", "multiply", "onesuperior", "twosuperior",
		"threesuperior", "onehalf", "onequarter", "threequarters",
		"franc", "Gbreve", "gbreve", "Idot",
		"Scedilla", "scedilla", "Cacute", "cacute",
		"Ccaron", "ccaron", "dmacron"
	];

	public function getTable()
	{
		$this->parseTableEntries();

		return $this->directory;
	}

	public function setTableOffset($offset)
	{
		$this->tableOffset = $offset;
	}

	public function parse()
	{
		$this->parseTableEntries();

		$this->data = [];

		foreach ($this->directory as $tag => $table) {
			if (empty($this->data[$tag])) {
				$this->readTable($tag);
			}
		}
	}

	public function utf8toUnicode($str)
	{
		$len = strlen($str);
		$out = [];

		for ($i = 0; $i < $len; $i++) {
			$uni = -1;
			$h   = ord($str[$i]);

			if ($h <= 0x7F) {
				$uni = $h;
			} elseif ($h >= 0xC2) {
				if (($h <= 0xDF) && ($i < $len - 1)) {
					$uni = ($h & 0x1F) << 6 | (ord($str[++$i]) & 0x3F);
				} elseif (($h <= 0xEF) && ($i < $len - 2)) {
					$uni = ($h & 0x0F) << 12 | (ord($str[++$i]) & 0x3F) << 6 | (ord($str[++$i]) & 0x3F);
				} elseif (($h <= 0xF4) && ($i < $len - 3)) {
					$uni = ($h & 0x0F) << 18 | (ord($str[++$i]) & 0x3F) << 12 | (ord($str[++$i]) & 0x3F) << 6 | (ord($str[++$i]) & 0x3F);
				}
			}

			if ($uni >= 0) {
				$out[] = $uni;
			}
		}

		return $out;
	}

	public function getUnicodeCharMap()
	{
		$subtable = null;
		foreach ($this->getData("cmap", "subtables") as $_subtable) {
			if ($_subtable["platformID"] == 0 || $_subtable["platformID"] == 3 && $_subtable["platformSpecificID"] == 1) {
				$subtable = $_subtable;
				break;
			}
		}

		if ($subtable) {
			return $subtable["glyphIndexArray"];
		}

		return null;
	}

	public function setSubset($subset)
	{
		if (!is_array($subset)) {
			$subset = $this->utf8toUnicode($subset);
		}

		$subset = array_unique($subset);

		$glyphIndexArray = $this->getUnicodeCharMap();

		if (!$glyphIndexArray) {
			return;
		}

		$gids = [
			0, // .notdef
			1, // .null
		];

		foreach ($subset as $code) {
			if (!isset($glyphIndexArray[$code])) {
				continue;
			}

			$gid        = $glyphIndexArray[$code];
			$gids[$gid] = $gid;
		}

		/** @var Glyf $glyf */
		$glyf = $this->getTableObject("glyf");
		$gids = $glyf->getGlyphIDs($gids);

		sort($gids);

		$this->glyph_subset = $gids;
		$this->glyph_all    = array_values($glyphIndexArray); // FIXME
	}

	public function getSubset()
	{
		if (empty($this->glyph_subset)) {
			return $this->glyph_all;
		}

		return $this->glyph_subset;
	}

	public function encode($tags = [])
	{
		if (!self::$raw) {
			$tags = array_merge(["head", "hhea", "cmap", "hmtx", "maxp", "glyf", "loca", "name", "post"], $tags);
		} else {
			$tags = array_keys($this->directory);
		}

		$num_tables = count($tags);
		$n = 16; // @todo

		Font::d("Tables : " . implode(", ", $tags));

		/** @var DirectoryEntry[] $entries */
		$entries = [];
		foreach ($tags as $tag) {
			if (!isset($this->directory[$tag])) {
				Font::d("  >> '$tag' table doesn't exist");
				continue;
			}

			$entries[$tag] = $this->directory[$tag];
		}

		$this->header->data["numTables"] = $num_tables;
		$this->header->encode();

		$directory_offset = $this->pos();
		$offset           = $directory_offset + $num_tables * $n;
		$this->seek($offset);

		$i = 0;
		foreach ($entries as $entry) {
			$entry->encode($directory_offset + $i * $n);
			$i++;
		}
	}

	public function parseHeader()
	{
		if (!empty($this->header)) {
			return;
		}

		$this->seek($this->tableOffset);

		$this->header = new Header($this);
		$this->header->parse();
	}

	public function getFontType()
	{
		$class_parts = explode("\\", get_class($this));
		return $class_parts[4];
	}

	public function parseTableEntries()
	{
		$this->parseHeader();

		if (!empty($this->directory)) {
			return;
		}

		if (empty($this->header->data["numTables"])) {
			return;
		}

		$type = $this->getFontType();

		$class = "isszz\\captcha\\font\\lib\\$type\\TableDirectoryEntry";

		for ($i = 0; $i < $this->header->data["numTables"]; $i++) {
			/** @var TableDirectoryEntry $entry */
			$entry = new $class($this);
			$entry->parse();

			$this->directory[$entry->tag] = $entry;
		}
	}

	public function normalizeFUnit($value, $base = 1000)
	{
		return round($value * ($base / $this->getData("head", "unitsPerEm")));
	}

	protected function readTable($tag)
	{
		$this->parseTableEntries();

		if (!self::$raw) {
			$name_canon = ucfirst(preg_replace("/[^a-z0-9]/", "", strtolower($tag)));
			
			$class = "isszz\\captcha\\font\\lib\\table\\type\\$name_canon";

			if (!isset($this->directory[$tag]) || !@class_exists($class)) {
				return;
			}
		} else {
			$class = "isszz\\captcha\\font\\lib\\table\\Table";
		}

		/** @var Table $table */
		$table = new $class($this->directory[$tag]);
		$table->parse();
		$this->data[$tag] = $table;
	}

	/**
	 * @param $name
	 *
	 * @return Table
	 */
	public function getTableObject($name)
	{
		return $this->data[$name];
	}

	public function setTableObject($name, Table $data)
	{
		$this->data[$name] = $data;
	}

	public function getData($name, $key = null)
	{
		$this->parseTableEntries();

		if (empty($this->data[$name])) {
			$this->readTable($name);
		}

		if (!isset($this->data[$name])) {
			return null;
		}

		if (!$key) {
			return $this->data[$name]->data;
		} else {
			return $this->data[$name]->data[$key];
		}
	}

	public function addDirectoryEntry(DirectoryEntry $entry)
	{
		$this->directory[$entry->tag] = $entry;
	}

	public function saveAdobeFontMetrics($file, $encoding = null)
	{
		$afm = new AdobeFontMetrics($this);
		$afm->write($file, $encoding);
	}

	/**
	 * Get a specific name table string value from its ID
	 *
	 * @param int $nameID The name ID
	 *
	 * @return string|null
	 */
	public function getNameTableString($nameID)
	{
		/** @var nameRecord[] $records */
		$records = $this->getData("name", "records");

		if (!isset($records[$nameID])) {
			return null;
		}

		return $records[$nameID]->string;
	}

	/**
	 * Get font copyright
	 *
	 * @return string|null
	 */
	public function getFontCopyright()
	{
		return $this->getNameTableString(name::NAME_COPYRIGHT);
	}

	/**
	 * Get font name
	 *
	 * @return string|null
	 */
	public function getFontName()
	{
		return $this->getNameTableString(name::NAME_NAME);
	}

	/**
	 * Get font subfamily
	 *
	 * @return string|null
	 */
	public function getFontSubfamily()
	{
		return $this->getNameTableString(name::NAME_SUBFAMILY);
	}

	/**
	 * Get font subfamily ID
	 *
	 * @return string|null
	 */
	public function getFontSubfamilyID()
	{
		return $this->getNameTableString(name::NAME_SUBFAMILY_ID);
	}

	/**
	 * Get font full name
	 *
	 * @return string|null
	 */
	public function getFontFullName()
	{
		return $this->getNameTableString(name::NAME_FULL_NAME);
	}

	/**
	 * Get font version
	 *
	 * @return string|null
	 */
	public function getFontVersion()
	{
		return $this->getNameTableString(name::NAME_VERSION);
	}

	/**
	 * Get font weight
	 *
	 * @return string|null
	 */
	public function getFontWeight()
	{
		return $this->getTableObject("OS/2")->data["usWeightClass"];
	}

	/**
	 * Get font Postscript name
	 *
	 * @return string|null
	 */
	public function getFontPostscriptName()
	{
		return $this->getNameTableString(Name::NAME_POSTSCRIPT_NAME);
	}

	public function reduce()
	{
		$names_to_keep = [
			Name::NAME_COPYRIGHT,
			Name::NAME_NAME,
			Name::NAME_SUBFAMILY,
			Name::NAME_SUBFAMILY_ID,
			Name::NAME_FULL_NAME,
			Name::NAME_VERSION,
			Name::NAME_POSTSCRIPT_NAME,
		];

		foreach ($this->data["name"]->data["records"] as $id => $rec) {
			if (!in_array($id, $names_to_keep)) {
				unset($this->data["name"]->data["records"][$id]);
			}
		}
	}
}
